import os.path
import copy
import re

# 注意：所有的目录名称都【不】以"/"结尾

# global选项
OPT_GLOBAL = 'global'
OPT_SOLUTION_NAME = 'solution_name'
OPT_PROJECTS = 'projects'
OPT_BEFORE_BUILD_SOLUTION_SCRIPT = 'before_build_solution_script'
OPT_AFTER_BUILD_SOLUTION_SCRIPT = 'after_build_solution_script'
OPT_EXTRA_CLEAN_SCRIPT = 'extra_clean_script'

# build_options，global和project公用
OPT_BUILD_OPTIONS = 'build_options'
OPT_CXX_COMPILER = 'cxx_compiler'
OPT_CXX_STANDARD = 'cxx_standard'
OPT_CXX_WARNING = 'cxx_warning'
OPT_C_COMPILER = 'c_compiler'
OPT_C_STANDARD = 'c_standard'
OPT_C_WARNING = 'c_warning'
OPT_OPT_LEVEL = 'opt_level'
OPT_ADD_DEBUG_SYMBOL = 'add_debug_symbol'
OPT_LIB_BUILDER = 'lib_builder'
OPT_DLL_BUILDER = 'dll_builder'
OPT_SCRIPT_SHELL = 'script_shell'
OPT_LINKER = 'linker'
OPT_LINK_OPTION = 'link_option'
OPT_EXTRA_COMPILE_FLAGS = 'extra_flags'

# project选项
OPT_PROJ_DIR = 'dir'
OPT_DEPENDENCY = 'dependency'
OPT_BUILD_SCRIPT = 'build_script'
OPT_BEFORE_BUILD_PROJ_SCRIPT = 'before_build_project_script'
OPT_AFTER_BUILD_PROJ_SCRIPT = 'after_build_project_script'
OPT_TYPE = 'type'
OPT_MGC = 'mgc'
OPT_MGC_SCRIPT = 'mgc_script'
OPT_DISABLE = 'disable'
OPT_IGNORE_SRC_FILES = 'ignore_src_files'
OPT_EXTRA_LIBS = 'extra_libs'
OPT_EXTRA_INC_DIRS = 'extra_inc_dirs'
OPT_REMOTE = 'remote'
OPT_PIPELINE = 'pipeline'

# global_options = {OPT_SOLUTION_NAME, OPT_PROJECTS,
#     OPT_PRE_BUILD_SOLUTION_SCRIPT, OPT_POST_BUILD_SOLUTION_SCRIPT,
#     OPT_EXTRA_CLEAN_SCRIPT}
# project_options = {OPT_NAME, OPT_CODE_LOCATION, OPT_OUTPUT_TYPE, 
#     OPT_DEPENDENCY, OPT_BUILD_SCRIPT, OPT_GIT_URL, OPT_GIT_ARGS, OPT_COMPILE_OPTION, 
#     OPT_PRE_BUILD_SCRIPT, OPT_POST_BUILD_SCRIPT, OPT_LINK_OPTION, OPT_IGNORE_SRC_FILES,
#     OPT_LIBS, OPT_INC_DIRS, OPT_MGC_SCRIPT, OPT_PROJ_DIR}
# shared_options = {OPT_C_COMPILER, OPT_CXX_COMPILER, OPT_LIB_BUILDER, OPT_DLL_BUILDER,
#     OPT_C_COMPILE_OPTION, OPT_CXX_COMPILE_OPTION, OPT_SCRIPT_SHELL}
# global_options = global_options.union(shared_options)
# project_options = project_options.union(shared_options)

DEFAULT_C_COMPILER          = 'gcc'
DEFAULT_C_STANDARD          = '-std=c11'
DEFAULT_C_WARNING           = '-Werror -Wall -Wextra'
DEFAULT_CXX_COMPILER        = 'g++'
DEFAULT_CXX_STANDARD        = '-std=c++11'
DEFAULT_CXX_WARNING         = '-Werror -Wall -Wextra'
DEFAULT_LINKER              = 'g++'
DEFAULT_LIB_BUILDER         = 'ar'
DEFAULT_DLL_BUILDER         = 'gcc'
DEFAULT_BUILD_SCRIPT        = ''
DEFAULT_BEFORE_BUILD_PROJ_SCRIPT    = ''
DEFAULT_AFTER_BUILD_PROJ_SCRIPT     = ''
OUTPUT_TYPE_SET             = {'exe', 'lib', 'dll', 'lib_dll', 'inc', 'mexe', 'other'}
DEFAULT_MGC                 = {}
DEFAULT_MGC_SCRIPT          = ''
DEFAULT_DISABLE             = False
DEFAULT_IGNORE_SRC_FILES    = []
DEFAULT_EXTRA_INC_DIRS      = []
DEFAULT_EXTRA_LIBS          = []
DEFAULT_REMOTE              = {}
# DEFAULT_PIPELINE            = ''
DEFAULT_LINK_OPTION         = ''
DEFAULT_SCRIPT_SHELL        = 'bash'
DEFAULT_DEPENDENCY          = []
DEFAULT_LIBS                = []
DEFAULT_INC_DIRS            = []
DEFAULT_BEFORE_BUILD_SOLUTION_SCRIPT    = ''
DEFAULT_AFTER_BUILD_SOLUTION_SCRIPT     = ''
DEFAULT_OPT_EXTRA_CLEAN_SCRIPT          = ''
DEFAULT_ADD_DEBUG_SYMBOL    = True
DEFAULT_OPT_LEVEL           = '-O0'
DEFAULT_EXTRA_COMPILE_FLAGS = ''
DEFAULT_BUILD_OPTIONS = {
    OPT_CXX_COMPILER: DEFAULT_CXX_COMPILER,
    OPT_CXX_STANDARD: DEFAULT_CXX_STANDARD,
    OPT_CXX_WARNING: DEFAULT_CXX_WARNING,
    OPT_C_COMPILER: DEFAULT_C_COMPILER,
    OPT_C_STANDARD: DEFAULT_C_STANDARD,
    OPT_C_WARNING: DEFAULT_C_WARNING,
    OPT_ADD_DEBUG_SYMBOL: DEFAULT_ADD_DEBUG_SYMBOL,
    OPT_OPT_LEVEL: DEFAULT_OPT_LEVEL,
    OPT_LINKER: DEFAULT_LINKER,
    OPT_LINK_OPTION: DEFAULT_LINK_OPTION,
    OPT_LIB_BUILDER: DEFAULT_LIB_BUILDER,
    OPT_DLL_BUILDER: DEFAULT_DLL_BUILDER,
    OPT_EXTRA_COMPILE_FLAGS: DEFAULT_EXTRA_COMPILE_FLAGS
}
DEFAULT_GLOBAL = {
    OPT_BEFORE_BUILD_SOLUTION_SCRIPT: DEFAULT_BEFORE_BUILD_SOLUTION_SCRIPT,
    OPT_AFTER_BUILD_SOLUTION_SCRIPT: DEFAULT_AFTER_BUILD_SOLUTION_SCRIPT,
    OPT_BUILD_OPTIONS: DEFAULT_BUILD_OPTIONS
}

key_stack = []

class SMConfigError(Exception):
    def __init__(self, message:str):
        location_message = ''
        if len(key_stack) != 0:
            for key in key_stack:
                location_message += '["%s"]'%(key)
        self.message = message
        if location_message != '':
            self.message += '  location: ' + location_message

def setIfNotExist(mapp, key, val, needStrip=False) -> None:
    '''检查mapp中是否存在key
    存在时，校验mapp[key]和val的类形是否一致；不存在时，设置mapp[key] = val
    val为str类形时，可以执行strip操作
    
    Raise:  SMConfigError'''
    key_stack.append(key)
    if key not in mapp:
        mapp[key] = copy.deepcopy(val)
    else:
        if type(mapp[key]) != type(val):
            raise SMConfigError('value type of "%s" should be "%s".'%(key, str(type(val))))
    if needStrip == True:
        if type(val) == str:
            mapp[key] = mapp[key].strip()
    key_stack.pop()

def setIfNotExistAndCheck(mapp:dict, key:str, val, checker):
    '''检查mapp中是否存在key，并调用检查函数'''
    setIfNotExist(mapp, key, val)
    checker(mapp[key])

def checkIfExist(mapp:dict, key:str, expected_type:type):
    if key not in mapp:
        raise SMConfigError('"%s" is missing.'%(key))
    key_stack.append(key)
    if expected_type != type(mapp[key]):
        raise SMConfigError('value type of "%s" should be "%s".'%(key, str(expected_type)))
    key_stack.pop()

def coverByDict(amap:dict, bmap:dict) -> dict:
    '''遍历bmap的每个key，检查在amap中是否存在，不存在则填入bmap的值'''
    for key in bmap:
        setIfNotExist(amap, key, bmap[key])

def checkConf(smake_conf:dict, solution_dir:str) -> tuple:
    '''检查配置文件，用默认值补充没有设置的字段
    Return: (conf, err_msg)
    '''
    # 检查必填字段，并为可选字段补充默认值
    global key_stack
    key_stack = []
    try: 
        
        name_checker = re.compile('[a-z,A-Z,_][a-z,A-Z,0-9,_]*')

        # 检查global内容
        setIfNotExistAndCheck(smake_conf, OPT_GLOBAL, DEFAULT_GLOBAL, checkGlobal)

        # 检查project内容
        if OPT_PROJECTS not in smake_conf:
            raise SMConfigError('"%s" is missing.'%(OPT_PROJECTS))
        key_stack.append(OPT_PROJECTS)
        for proj_name in smake_conf[OPT_PROJECTS]:
            checkName(proj_name, name_checker)
            key_stack.append(proj_name)
            checkProject(smake_conf[OPT_PROJECTS][proj_name], proj_name, solution_dir, smake_conf[OPT_GLOBAL][OPT_BUILD_OPTIONS])
            key_stack.pop()
        key_stack.pop()
        
        # 检查配置逻辑是否合理
        # 依赖的项目名必须存在
        proj_names = set(smake_conf[OPT_PROJECTS].keys())
        for proj_name in proj_names:
            proj_info = smake_conf[OPT_PROJECTS][proj_name]
            for dep_proj_name in proj_info[OPT_DEPENDENCY]:
                if dep_proj_name not in proj_names:
                    raise SMConfigError('dependency project name "%s" not exist.'%(dep_proj_name))
    except SMConfigError as e:
        return (None, e.message)
    return (smake_conf, '')

def checkGlobal(conf) -> str:
    '''检查'global'字段中的内容，用默认值补充没有设置的字段
    
    Args:
        conf: smake_conf['glboal']中的内容'''
    key_stack.append(OPT_GLOBAL)
    setIfNotExistAndCheck(conf, OPT_BUILD_OPTIONS, DEFAULT_BUILD_OPTIONS, checkBuildOptions)
    setIfNotExist(conf, OPT_BEFORE_BUILD_SOLUTION_SCRIPT, DEFAULT_BEFORE_BUILD_SOLUTION_SCRIPT)
    setIfNotExist(conf, OPT_AFTER_BUILD_SOLUTION_SCRIPT, DEFAULT_AFTER_BUILD_SOLUTION_SCRIPT)
    key_stack.pop()

def checkName(name, name_checker):
    '''检查名字是否符合C变量名规则'''
    if type(name) != str:
        raise SMConfigError('project name "%s" should be string.'%(name))
    if name_checker.fullmatch(name) == None:
        raise SMConfigError('project name "%s" is illegal.'%(name))

def checkProject(conf: dict, proj_name:str, solution_dir:str, globle_build_options:dict):
    '''检查project info中的内容，用默认值补充没有设置的字段
    
    Args:
        conf: smake_conf['projects'][proj_name]中的内容
    Raise: SMConfigError'''
    setIfNotExist(conf, OPT_PROJ_DIR, os.path.join(solution_dir, proj_name))
    setIfNotExist(conf, OPT_DEPENDENCY, DEFAULT_DEPENDENCY)
    setIfNotExist(conf, OPT_BUILD_SCRIPT, DEFAULT_BUILD_SCRIPT)
    setIfNotExist(conf, OPT_BEFORE_BUILD_PROJ_SCRIPT, DEFAULT_BEFORE_BUILD_PROJ_SCRIPT)
    setIfNotExist(conf, OPT_AFTER_BUILD_PROJ_SCRIPT, DEFAULT_AFTER_BUILD_PROJ_SCRIPT)
    checkIfExist(conf, OPT_TYPE, str)
    if conf[OPT_TYPE] not in OUTPUT_TYPE_SET:
        raise SMConfigError('project type "%s" is illegal.'%(conf[OPT_TYPE]))
    setIfNotExistAndCheck(conf, OPT_MGC, DEFAULT_MGC, checkMgc)
    setIfNotExist(conf, OPT_DISABLE, DEFAULT_DISABLE)
    setIfNotExist(conf, OPT_IGNORE_SRC_FILES, DEFAULT_IGNORE_SRC_FILES)
    setIfNotExist(conf, OPT_EXTRA_INC_DIRS, DEFAULT_EXTRA_INC_DIRS)
    setIfNotExist(conf, OPT_EXTRA_LIBS, DEFAULT_EXTRA_LIBS)

    # 用global的build_option覆盖project的build_option
    setIfNotExist(conf, OPT_BUILD_OPTIONS, {})
    coverByDict(conf[OPT_BUILD_OPTIONS], globle_build_options)
    checkBuildOptions(conf[OPT_BUILD_OPTIONS])

def checkMgc(conf: dict):
    # 空dict表示不启用mgc，不做检查
    if conf == {}:
        return
    # 暂时强制要求有mgc_script
    key_stack.append(OPT_MGC)
    checkIfExist(conf, OPT_MGC_SCRIPT, str)
    key_stack.pop()
    
def checkBuildOptions(conf: dict) -> str:
    key_stack.append(OPT_BUILD_OPTIONS)
    setIfNotExist(conf, OPT_CXX_COMPILER, DEFAULT_CXX_COMPILER, needStrip=True)
    setIfNotExist(conf, OPT_CXX_STANDARD, DEFAULT_CXX_STANDARD, needStrip=True)
    setIfNotExist(conf, OPT_CXX_WARNING, DEFAULT_CXX_WARNING, needStrip=True)
    setIfNotExist(conf, OPT_C_COMPILER, DEFAULT_C_COMPILER, needStrip=True)
    setIfNotExist(conf, OPT_C_STANDARD, DEFAULT_C_STANDARD, needStrip=True)
    setIfNotExist(conf, OPT_C_WARNING, DEFAULT_C_WARNING, needStrip=True)
    setIfNotExist(conf, OPT_ADD_DEBUG_SYMBOL, DEFAULT_ADD_DEBUG_SYMBOL)
    setIfNotExist(conf, OPT_OPT_LEVEL, DEFAULT_OPT_LEVEL, needStrip=True)
    setIfNotExist(conf, OPT_LINKER, DEFAULT_LINKER, needStrip=True)
    setIfNotExist(conf, OPT_LIB_BUILDER, DEFAULT_LIB_BUILDER, needStrip=True)
    setIfNotExist(conf, OPT_DLL_BUILDER, DEFAULT_DLL_BUILDER, needStrip=True)
    setIfNotExist(conf, OPT_LINK_OPTION, DEFAULT_LINK_OPTION, needStrip=True)
    setIfNotExist(conf, OPT_EXTRA_COMPILE_FLAGS, DEFAULT_EXTRA_COMPILE_FLAGS, needStrip=True)
    key_stack.pop()
